package it.tukano.treditor.modules;

import it.tukano.app.AbstractAppModule;
import it.tukano.app.AppModule;
import it.tukano.check.Checks;
import it.tukano.fun.VFunction1;
import it.tukano.treditor.engine.EngineQueryInterface;
import it.tukano.treditor.engine.geom.BoundingSphere;
import it.tukano.treditor.engine.geom.EulerSet;
import it.tukano.treditor.engine.geom.GridBuilder;
import it.tukano.treditor.engine.geom.NTuple3;
import it.tukano.treditor.engine.scene.DirectionalLightElement;
import it.tukano.treditor.engine.scene.MeshElement;
import it.tukano.treditor.engine.scene.ModelElement;
import it.tukano.treditor.engine.scene.NodeElement;
import it.tukano.treditor.engine.scene.SceneElement;
import it.tukano.treditor.engine.scene.SceneView;
import it.tukano.treditor.modules.datamodel.DataModelEvent;
import it.tukano.treditor.modules.datamodel.DataModelEvent.EventType;
import it.tukano.treditor.modules.datamodel.DataModelListener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * This module holds the application's data and notify registered listeners
 * of data modifications
 * @author pgi
 */
public class DataModel extends AbstractAppModule {
    
    /* Default scene root */
    private NodeElement root = new NodeElement();
    
    /* Default temporary elements root */
    private NodeElement tempRoot = new NodeElement();
    
    /* Default xy grid */
    private final GridBuilder gridBuilder = GridBuilder.newInstance();
    private final MeshElement xyGrid = new MeshElement().setMesh(gridBuilder.generateXYGrid(1, 100));
    private final MeshElement yzGrid = new MeshElement().setMesh(gridBuilder.generateYZGrid(1, 100));
    private final MeshElement xzGrid = new MeshElement().setMesh(gridBuilder.generateXZGrid(1, 100));
    
    
    /* Listeners */
    private final List<DataModelListener> LISTENERS = new CopyOnWriteArrayList<>();
    
    /* The current view */
    private SceneView currentView;
    private SceneElement selectedElement;

    /**
     * Instance initializer
     */
    public DataModel() {
        root.setName("scene root");
        tempRoot.setName("temp root");
    }

    @Override
    public void startModule(AppModule context) {
        super.startModule(context);
        setSceneRoot(this, root);
        notifyTempRootSet(this, tempRoot);
    }
    
    /** Returns the temporary elements' node. This is used to attach ui related
     effects (such as grids and arrows) to the scene. */
    public NodeElement getTempRoot() {
        return tempRoot;
    }
    
    /**
     * Add a listener to the model. Thread-Safe
     * @param listener the listener to add
     */
    public void addListener(DataModelListener listener) {
        LISTENERS.add(listener);
    }
    
    /**
     * Removes a listener from the model. Thread-Safe
     * @param listener 
     */
    public void removeListener(DataModelListener listener) {
        LISTENERS.remove(listener);
    }

    /**
     * Set the root of the scene and notifies the listeners. Thread-Safe
     * @param notifier the object that request the notify
     * @param newRoot the new root node
     */
    public void setSceneRoot(Object notifier, NodeElement newRoot) {
        if(notifier != this) {
            root = newRoot;
        }
        notifyDataModelEvent(new DataModelEvent(notifier, DataModelEvent.EventType.SCENE_ROOT_SET, newRoot));
    }
    
    /**
     * Sets the temporary elements' root and notifies the listeners
     * @param notifier the source of the notify
     * @param newTempRoot the new temporary root element
     */
    public void notifyTempRootSet(Object notifier, NodeElement newTempRoot) {
        if(notifier != this) {
            tempRoot = newTempRoot;
        }
        notifyDataModelEvent(new DataModelEvent(notifier, DataModelEvent.EventType.TEMP_ROOT_SET, newTempRoot));
    }

    /**
     * Sends an event to the listener. Thread-Safe
     * @param dataModelEvent the event to send
     */
    private void notifyDataModelEvent(DataModelEvent dataModelEvent) {
        for (DataModelListener dataModelListener : LISTENERS) {
            dataModelListener.dataModelEventPerformed(dataModelEvent);
        }
    }

    public void notifyEmptyNodeAddToNode(Object notifier, NodeElement newNode, NodeElement parentNode) {
        Checks.numericEquals(newNode.getChildrenCount(), 0);
        notifyDataModelEvent(new DataModelEvent(notifier, DataModelEvent.EventType.NODE_ADD_EMPTY_NODE, parentNode, newNode));
    }
    
    public void notifyModelElementAddToNode(Object notifier, ModelElement model, NodeElement node) {
        notifyDataModelEvent(new DataModelEvent(notifier, DataModelEvent.EventType.NODE_ADD_MODEL, node, model));
    }
    
    public void notifyDirectionalLightAddToNode(Object notifier, DirectionalLightElement lightElement, NodeElement nodeElement) {
        notifyDataModelEvent(new DataModelEvent(notifier, EventType.NODE_ADD_DIRECTIONAL_LIGHT, nodeElement, lightElement));
    }
    
    public void setSelectedElement(Object notifier, SceneElement selectedElement) {
        if(selectedElement == this.selectedElement) return;
        
        this.selectedElement = selectedElement;
        if(selectedElement == null) {
            notifyDataModelEvent(new DataModelEvent(notifier, EventType.SELECTED_ELEMENT_SET));
        } else {
            notifyDataModelEvent(new DataModelEvent(notifier, EventType.SELECTED_ELEMENT_SET, selectedElement));
        }
    }

    public void setCurrentView(Object notifier, SceneView view) {
        this.currentView = view;
        notifyDataModelEvent(new DataModelEvent(notifier, EventType.CURRENT_VIEW_SET, view));
    }

    public SceneElement getSceneRoot() {
        return root;
    }
    
    public void notifyModelTransformUpdate(Object notifier, ModelElement updatedModel) {
        notifyDataModelEvent(new DataModelEvent(notifier, EventType.MODEL_TRANSFORM_UPDATE, updatedModel));
    }

    public void notifyDirectionalLightUpdated(Object notifier, DirectionalLightElement editedLight) {
        notifyDataModelEvent(new DataModelEvent(notifier, EventType.DIRECTIONAL_LIGHT_UPDATE, editedLight));
    }
    
    public void notifyElementRemovedFromNode(Object notifier, SceneElement removedElement, NodeElement oldParent) {
        notifyDataModelEvent(new DataModelEvent(notifier, EventType.ELEMENT_REMOVED_FROM_NODE, removedElement, oldParent));
    }

    public void setXYGridState(Object sender, boolean show) {
        setGridState(sender, show, xyGrid);
    }
    
    public void setXZGridState(Object sender, boolean show) {
        setGridState(sender, show, xzGrid);
    }
    
    public void setYZGridState(Object sender, boolean show) {
        setGridState(sender, show, yzGrid);
    }
    
    private void setGridState(Object sender, boolean show, SceneElement grid) {
        if(show) {
            tempRoot.addChild(grid);
            notifyDataModelEvent(new DataModelEvent(sender, EventType.NODE_ADD_MESH, tempRoot, grid));
        } else {
            tempRoot.removeChild(grid);
            notifyDataModelEvent(new DataModelEvent(sender, EventType.ELEMENT_REMOVED_FROM_NODE, grid, tempRoot));
        }
    }

    public SceneView getCurrentView() {
        return currentView;
    }

    public void setCurrentViewToLookAtElement(final Object caller, SceneElement element) {
        EngineManager engineManager = find(EngineManager.class);
        if(engineManager == null) return;
        
        EngineQueryInterface eqi = engineManager.getEngineQueryInterface();
        if(eqi == null) return;
        
        eqi.getElementBoundingSphere(element, new VFunction1<BoundingSphere>() {

            @Override
            protected void doApply(BoundingSphere p) {
                
                Number radius = p.getRadius();
                NTuple3 center = p.getCenter();
                float distance = 2.75f * radius.floatValue();
                
                float x = center.getX().floatValue() - distance;
                float y = center.getY().floatValue() + distance;
                float z = center.getZ().floatValue() - distance;
                
                NTuple3 location = new NTuple3(x, y, z);
                EulerSet orientation  = new EulerSet(30, 45, 0, false);
                
                SceneView view = getCurrentView();
                view.setLocation(location);
                view.setOrientation(orientation);
                setCurrentView(caller, view);
            }
        });
    }
}
